<!DOCTYPE html>
<meta charset=utf-8>
<head>
  <script type="text/javascript" src="u2futil.js"></script>
  <script type="text/javascript" src="pkijs/common.js"></script>
  <script type="text/javascript" src="pkijs/asn1.js"></script>
  <script type="text/javascript" src="pkijs/x509_schema.js"></script>
  <script type="text/javascript" src="pkijs/x509_simpl.js"></script>
</head>
<body>
<p>Register and Sign Test for FIDO Universal Second Factor</p>
<script class="testbody" type="text/javascript">
"use strict";

var state = {
  // Raw messages
  regRequest: null,
  regResponse: null,

  regKey: null,
  signChallenge: null,
  signResponse: null,

  // Parsed values
  publicKey: null,
  keyHandle: null,

  // Constants
  version: "U2F_V2",
  appId: window.location.origin,
};

SpecialPowers.pushPrefEnv({"set": [["security.webauth.u2f", true],
                                   ["security.webauth.u2f_enable_softtoken", true]]},
function() {
  local_isnot(window.u2f, undefined, "U2F API endpoint must exist");
  local_isnot(window.u2f.register, undefined, "U2F Register API endpoint must exist");
  local_isnot(window.u2f.sign, undefined, "U2F Sign API endpoint must exist");

  testRegistering();

  function testRegistering() {
    var challenge = new Uint8Array(16);
    window.crypto.getRandomValues(challenge);

    state.regRequest = {
      version: state.version,
      challenge: bytesToBase64UrlSafe(challenge),
    };

    u2f.register(state.appId, [state.regRequest], [], function(regResponse) {
      state.regResponse = regResponse;

      local_is(regResponse.errorCode, 0, "The registration did not error");
      local_isnot(regResponse.registrationData, undefined, "The registration did not provide registration data");
      if (regResponse.errorCode > 0) {
        local_finished();
        return;
      }

      // Parse the response data from the U2F token
      var registrationData = base64ToBytesUrlSafe(regResponse.registrationData);
      local_is(registrationData[0], 0x05, "Reserved byte is correct")

      state.publicKeyBytes = registrationData.slice(1, 66);
      var keyHandleLength = registrationData[66];
      state.keyHandleBytes = registrationData.slice(67, 67 + keyHandleLength);
      state.keyHandle = bytesToBase64UrlSafe(state.keyHandleBytes);
      state.attestation = registrationData.slice(67 + keyHandleLength);

      local_is(state.attestation[0], 0x30, "Attestation Certificate has correct starting byte");
      var asn1 = org.pkijs.fromBER(state.attestation.buffer);
      console.log(asn1);
      state.attestationCert = new org.pkijs.simpl.CERT({ schema: asn1.result });
      console.log(state.attestationCert);
      state.attestationSig = state.attestation.slice(asn1.offset);
      local_is(state.attestationCert.subject.types_and_values[0].value.value_block.value, "Firefox U2F Soft Token", "Expected Subject");
      local_is(state.attestationCert.issuer.types_and_values[0].value.value_block.value, "Firefox U2F Soft Token", "Expected Issuer");
      local_is(state.attestationCert.notAfter.value - state.attestationCert.notBefore.value, 1000*60*60*48, "Valid 48 hours (in millis)");

      // Verify that the clientData from the U2F token makes sense
      var clientDataJSON = "";
      base64ToBytesUrlSafe(regResponse.clientData).map(x => clientDataJSON += String.fromCharCode(x));
      var clientData = JSON.parse(clientDataJSON);
      local_is(clientData.typ, "navigator.id.finishEnrollment", "Register - Data type matches");
      local_is(clientData.challenge, state.regRequest.challenge, "Register - Challenge matches");
      local_is(clientData.origin, window.location.origin, "Register - Origins are the same");

      // Verify the signature from the attestation certificate
      deriveAppAndChallengeParam(state.appId, string2buffer(clientDataJSON))
      .then(function(params){
        state.appParam = params.appParam;
        state.challengeParam = params.challengeParam;
        return state.attestationCert.getPublicKey();
      }).then(function(attestationPublicKey) {
        var signedData = assembleRegistrationSignedData(state.appParam, state.challengeParam, state.keyHandleBytes, state.publicKeyBytes);
        return verifySignature(attestationPublicKey, signedData, state.attestationSig);
      }).then(function(verified) {
        console.log("No error verifying signature");
        local_ok(verified, "Attestation Certificate signature verified")
         // Import the public key of the U2F token into WebCrypto
        return importPublicKey(state.publicKeyBytes)
      }).then(function(key) {
        state.publicKey = key;
        local_ok(true, "Imported public key")

        // Ensure the attestation certificate is properly self-signed
        return state.attestationCert.verify()
      }).then(function(){
        local_ok(true, "Attestation Certificate verification successful");

        // Continue test
        testReRegister()
      }).catch(function(err){
        console.log(err);
        local_ok(false, "Attestation Certificate verification failed");
        local_finished();
      });
    });
  }

  function testReRegister() {
    state.regKey = {
      version: state.version,
      keyHandle: state.keyHandle,
    };

    // Test that we don't re-register if we provide regKey as an
    // "already known" key handle. The U2F module should recognize regKey
    // as being usable and, thus, give back errorCode 4.
    u2f.register(state.appId, [state.regRequest], [state.regKey], function(regResponse) {
      // Since we attempted to register with state.regKey as a known key, expect
      // ineligible (=4).
      local_is(regResponse.errorCode, 4, "The re-registration should show device ineligible");
      local_is(regResponse.registrationData, undefined, "The re-registration did not provide registration data");

      // Continue test
      testSigning();
    });
  }

  function testSigning() {
    var challenge = new Uint8Array(16);
    window.crypto.getRandomValues(challenge);
    state.signChallenge = bytesToBase64UrlSafe(challenge);

    // Now try to sign the signature challenge
    u2f.sign(state.appId, state.signChallenge, [state.regKey], function(signResponse) {
      state.signResponse = signResponse;

      // Make sure this signature op worked, bailing early if it failed.
      local_is(signResponse.errorCode, 0, "The signing did not error");
      local_isnot(signResponse.clientData, undefined, "The signing did not provide client data");

      if (signResponse.errorCode > 0) {
        local_finished();
        return;
      }

      // Decode the clientData that was returned from the module
      var clientDataJSON = "";
      base64ToBytesUrlSafe(signResponse.clientData).map(x => clientDataJSON += String.fromCharCode(x));
      var clientData = JSON.parse(clientDataJSON);
      local_is(clientData.typ, "navigator.id.getAssertion", "Sign - Data type matches");
      local_is(clientData.challenge, state.signChallenge, "Sign - Challenge matches");
      local_is(clientData.origin, window.location.origin, "Sign - Origins are the same");

      // Parse the signature data
      var signatureData = base64ToBytesUrlSafe(signResponse.signatureData);
      if (signatureData[0] != 0x01) {
        throw "User presence byte not set";
      }
      var presenceAndCounter = signatureData.slice(0,5);
      var signatureValue = signatureData.slice(5);

      // Assemble the signed data and verify the signature
      deriveAppAndChallengeParam(state.appId, string2buffer(clientDataJSON))
      .then(function(params){
        return assembleSignedData(params.appParam, presenceAndCounter, params.challengeParam);
      })
      .then(function(signedData) {
        return verifySignature(state.publicKey, signedData, signatureValue);
      })
      .then(function(verified) {
        console.log("No error verifying signing signature");
        local_ok(verified, "Signing signature verified")

        local_finished();
      })
      .catch(function(err) {
        console.log(err);
        local_ok(false, "Signing signature invalid");
        local_finished();
      });
    });
  }
});

</script>
</body>
</html>
